﻿using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Xml;
using System.Xml.Schema;
using SDCloudSolution.SDCloud.BusinessLayer.Interfaces;
using SDCloudSolution.SDCloud.BusinessLayer.Model.Interfaces;
using SDCloudSolution.SDCloud.Entities;
using SDCloudSolution.SDCloud.Entities.Models;
using SDCloudSolution.SDCloud.Web.Infrastructure.Interfaces;
using SDCloudSolution.SDCloud.Web.Models;
using SDCloudSolution.SDCloud.Web.Models.Model;
using SDCloudSolution.SDCloud.Web.Models.ModelRun;
using SDCloudSolution.SDCloud.DataLayer.Model;

namespace SDCloudSolution.SDCloud.Web.Infrastructure
{
    public class ModelViewModelProvider : IModelViewModelProvider
    {
        private readonly IUserIdentityProvider _userIdentityProvider;
        private readonly IModelComponent _modelComponent;
        private readonly IModelRunViewModelProvider _modelRunViewModelProvider;
        private readonly IModelStatisticsViewModelProvider _modelStatisticsViewModelProvider;
        private readonly IModelFileStorageProvider _modelFileStorageProvider;
        private readonly ITextProcessor _textProcessor;
        private readonly IDateTimeFormatter _dateTimeFormatter;

        public ModelViewModelProvider(
            IUserIdentityProvider userIdentityProvider, 
            IModelComponent modelComponent,
            IModelRunViewModelProvider modelRunViewModelProvider,
            IModelStatisticsViewModelProvider modelStatisticsViewModelProvider,
            IModelFileStorageProvider modelFileStorageProvider,
            ITextProcessor textProcessor,
            IDateTimeFormatter dateTimeFormatter)
        {
            _userIdentityProvider = userIdentityProvider;
            _modelComponent = modelComponent;
            _modelRunViewModelProvider = modelRunViewModelProvider;
            _modelStatisticsViewModelProvider = modelStatisticsViewModelProvider;
            _modelFileStorageProvider = modelFileStorageProvider;
            _textProcessor = textProcessor;
            _dateTimeFormatter = dateTimeFormatter;
        }

        public async Task<ModelListViewModel> GetModelsListViewModelAsync(PagingModel paging)
        {
            var collection = await _modelComponent.FindModelsForUser(
                _userIdentityProvider.Current.UserId, 
                new Paging(paging.PageIndex, paging.PageSize));

            return ConvertToViewModel(collection);
        }

        public async Task<int> CopyPublicModelToPrivateCollection(int publicModelId)
        {
            if (publicModelId <= 0)
            {
                return 0;
            }

            var originalModel = await _modelComponent.GetModelByIdAsync(publicModelId);
            if (originalModel == null || originalModel.Id != publicModelId || !originalModel.IsPublic)
            {
                return 0;
            }

            var countOfModelCopies = await _modelComponent.GetCountOfModelCopiesForUser(originalModel.Id, _userIdentityProvider.Current.UserId);

            var copyOfModel = new ModelEntity
            {
                Name = countOfModelCopies == 0 
                    ? $"Copy of {originalModel.Name}"
                    : $"Copy of {originalModel.Name} ({countOfModelCopies})",
                CreatedDateUtc = originalModel.CreatedDateUtc,
                Description = originalModel.Description,
                FileName = originalModel.FileName,
                OriginalModelId = originalModel.Id,
                IsPublic = false,
                ModifyDateUtc = DateTime.Now,
                UserId = _userIdentityProvider.Current.UserId
            };

            await _modelComponent.SaveModelAsync(copyOfModel);

            using (var modelFileStream = _modelFileStorageProvider.GetModelFile(originalModel))
            {
                _modelFileStorageProvider.UploadModelFile(copyOfModel, modelFileStream);
            }

            return copyOfModel.Id;
        }

        public async Task<ModelListViewModel> GetPublicModelsListViewModelAsync(PagingModel paging)
        {
            var collection = await _modelComponent.FindPublicModelsAsync(new Paging(paging.PageIndex, paging.PageSize));

            return ConvertToViewModel(collection);
        }

        public async Task<ModelEditViewModel> GetModelEditViewModelAsync(int modelId = 0)
        {
            if (modelId == 0)
            {
                return new ModelEditViewModel
                {
                    ModelId = 0,
                    Name = string.Empty,
                    Description = string.Empty
                };
            }

            var modelEntity = await _modelComponent.GetModelByIdAsync(modelId);
            if (modelEntity == null || modelEntity.Id != modelId)
            {
                return null;
            }

            if (_userIdentityProvider.Current.UserId != modelEntity.UserId)
            {
                throw new UnauthorizedAccessException();
            }

            return new ModelEditViewModel
            {
                ModelId = modelEntity.Id,
                Name = modelEntity.Name,
                Description = modelEntity.Description,
                CurrentFileName = modelEntity.FileName
            };
        }

        public async Task<ModelViewModel> GetModelViewModelAsync(int modelId)
        {
            if (modelId <= 0)
            {
                return null;
            }

            var modelEntity = await _modelComponent.GetModelByIdAsync(modelId);
            if (modelEntity == null || modelEntity.Id != modelId)
            {
                return null;
            }

            if (_userIdentityProvider.Current.UserId != modelEntity.UserId)
            {
                throw new UnauthorizedAccessException();
            }

            var lastModelRunItem = await _modelRunViewModelProvider.GetLastModelRunForModelAsync(modelId);

            var modelStatistics = await _modelStatisticsViewModelProvider.GetModelStatisticsForModelAsync(modelId);

            return ConvertToViewModel(modelEntity, lastModelRunItem, modelStatistics);
        }

        public async Task<ModelViewModel> GetPublicModelViewModelAsync(int publicModelId)
        {
            if (publicModelId <= 0)
            {
                throw new UnauthorizedAccessException();
            }

            var modelEntity = await _modelComponent.GetModelByIdAsync(publicModelId);
            if (modelEntity == null || modelEntity.Id != publicModelId)
            {
                return null;
            }

            if (!modelEntity.IsPublic)
            {
                throw new UnauthorizedAccessException();
            }

            var modelStatistics = await _modelStatisticsViewModelProvider.GetModelStatisticsForPublicModelAsync(publicModelId);

            return ConvertToViewModel(modelEntity, null, modelStatistics);
        }

        public async Task<Stream> GetModelFileStreamAsync(int modelId)
        {
            if (modelId <= 0)
            {
                throw new ArgumentNullException();
            }

            var modelEntity = await _modelComponent.GetModelByIdAsync(modelId);
            if (modelEntity == null || modelEntity.Id != modelId)
            {
                return null;
            }

            if (!modelEntity.IsPublic && _userIdentityProvider.Current.UserId != modelEntity.UserId)
            {
                throw new UnauthorizedAccessException();
            }

            return _modelFileStorageProvider.GetModelFile(modelEntity);
        }

        public void ValidateModelUploadedFile(ModelEditViewModel editViewModel, ModelStateDictionary modelState)
        {
            if (!IsFileUploaded(editViewModel.ModelFile))
            {
                if (editViewModel.ModelId == 0)
                {
                    modelState.AddModelError(nameof(editViewModel.ModelFile), "Model file should be specified.");
                }

                return;
            }

            var extension = Path.GetExtension(editViewModel.ModelFile.FileName);

            // TODO Extract to factory method
            if (extension == ".xmile")
            {
                ValidateXmileModel(editViewModel, modelState);
                return;
            }

            if (extension == ".mdl")
            {
                ValidateVensimModel(editViewModel, modelState);
                return;
            }

            modelState.AddModelError(nameof(editViewModel.ModelFile), "Uploaded file have unknown format. You should upload the XMILE or VenSim file model.");
        }

        private void ValidateVensimModel(ModelEditViewModel editViewModel, ModelStateDictionary modelState)
        {
            // TODO Implement some validation
        }

        private void ValidateXmileModel(ModelEditViewModel editViewModel, ModelStateDictionary modelState)
        {
            var settings = new XmlReaderSettings();
            var xmileXsdFilePath = HostingEnvironment.MapPath("~/Resources/xmile.xsd");
            if (string.IsNullOrWhiteSpace(xmileXsdFilePath) || !File.Exists(xmileXsdFilePath))
            {
                throw new InvalidOperationException("Can't found the validation schema file for XMILE format");
            }

            ValidationEventHandler validationEventHandler = (sender, args) =>
            {
                if (args.Severity != XmlSeverityType.Error)
                {
                    return;
                }

                var errorMessage = args.Exception != null
                    ? $"{args.Exception.Message} at position {args.Exception.LinePosition} on line {args.Exception.LineNumber}"
                    : args.Message;

                modelState.AddModelError(nameof(editViewModel.ModelFile), errorMessage);
            };

            settings.Schemas.Add("http://docs.oasis-open.org/xmile/ns/XMILE/v1.0", xmileXsdFilePath);
            settings.ValidationType = ValidationType.Schema;
            settings.CloseInput = false;
            settings.IgnoreComments = true;
            settings.ValidationEventHandler += validationEventHandler;

            using (var reader = XmlReader.Create(editViewModel.ModelFile.InputStream, settings))
            {
                try
                {
                    var document = new XmlDocument();
                    document.Load(reader);
                }
                catch (XmlSchemaValidationException)
                {
                    modelState.AddModelError(nameof(editViewModel.ModelFile), "Uploaded file should have the XMILE format");
                }
                catch (XmlException)
                {
                    modelState.AddModelError(nameof(editViewModel.ModelFile), "Uploaded file should have the XMILE format");
                }
            }
        }

        public async Task<ModelEntity> SaveAsync(ModelEditViewModel editViewModel)
        {
            ModelEntity modelEntity;

            var isFileUploaded = IsFileUploaded(editViewModel.ModelFile);

            if (editViewModel.ModelId > 0)
            {
                modelEntity = await _modelComponent.GetModelByIdAsync(editViewModel.ModelId);
                if (modelEntity == null || modelEntity.Id != editViewModel.ModelId)
                {
                    return null;
                }

                if (modelEntity.UserId != _userIdentityProvider.Current.UserId)
                {
                    throw new UnauthorizedAccessException();
                }

                modelEntity.ModifyDateUtc = DateTime.UtcNow;
                modelEntity.Name = editViewModel.Name;
                modelEntity.Description = editViewModel.Description;

                if (isFileUploaded)
                {
                    if (!string.IsNullOrWhiteSpace(modelEntity.FileName))
                    {
                        _modelFileStorageProvider.DeleteModelFile(modelEntity);
                    }

                    modelEntity.FileName = editViewModel.ModelFile.FileName;
                }
            }
            else
            {
                modelEntity = new ModelEntity
                {
                    Id = 0,
                    UserId = _userIdentityProvider.Current.UserId,
                    CreatedDateUtc = DateTime.UtcNow,
                    Name = editViewModel.Name,
                    Description = editViewModel.Description,
                    FileName = editViewModel.ModelFile.FileName
                };
            }

            await _modelComponent.SaveModelAsync(modelEntity);

            if (isFileUploaded)
            {
                _modelFileStorageProvider.UploadModelFile(modelEntity, editViewModel.ModelFile.InputStream);
            }

            return modelEntity;
        }

        public async Task DeleteAsync(int modelId)
        {
            if (modelId <= 0)
            {
                throw new UnauthorizedAccessException();
            }

            var modelEntity = await _modelComponent.GetModelByIdAsync(modelId);
            if (modelEntity == null)
            {
                throw new UnauthorizedAccessException();
            }

            if (modelEntity.UserId != _userIdentityProvider.Current.UserId)
            {
                throw new UnauthorizedAccessException();
            }

            await _modelComponent.DeleteAsync(modelEntity);
        }

        private bool IsFileUploaded(HttpPostedFileBase uploadedFile)
        {
            return uploadedFile != null && uploadedFile.ContentLength > 0;
        }

        private ModelListItemViewModel ConvertToListItemViewModel(ModelEntity entity)
        {
            return new ModelListItemViewModel
            {
                ModelId = entity.Id,
                Name = entity.Name,
                CreatedDate = _dateTimeFormatter.ToFullDateTimeFormat(entity.CreatedDateUtc),
                ModifyDate = entity.ModifyDateUtc.HasValue
                    ? _dateTimeFormatter.ToFullDateTimeFormat(entity.ModifyDateUtc.Value)
                    : string.Empty
            };
        }

        private ModelViewModel ConvertToViewModel(ModelEntity entity, ModelRunListItemViewModel lastModelRunEntity, ModelStatisticsViewModel modelStatisticsViewModel)
        {
            return new ModelViewModel
            {
                ModelId = entity.Id,
                Name = entity.Name,
                IsPublic = entity.IsPublic,
                OriginalModelId = entity.OriginalModelId,
                Description = entity.Description,
                FormattedDescription = _textProcessor.ProcessText(entity.Description),
                FileName = entity.FileName,
                CanRunModel = lastModelRunEntity == null || lastModelRunEntity.IsCompleted,
                CanStopLastModelExecution = lastModelRunEntity != null && !lastModelRunEntity.IsCompleted,
                LastModelRunItem = lastModelRunEntity,
                ModelStatistics = modelStatisticsViewModel
            };
        }

        private ModelListViewModel ConvertToViewModel(EntityCollection<ModelEntity> collection)
        {
            return new ModelListViewModel
            {
                Items = collection.EntityList.Select(ConvertToListItemViewModel).ToList(),
                Paging = new PagingViewModel
                {
                    PageIndex = collection.PageIndex,
                    PageSize = collection.PageSize,
                    TotalCount = collection.TotalCount
                }
            };
        }
    }
}